﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using NHibernate.Identity.Internal;

namespace NHibernate.Identity
{
    public class UserStore<TUser> : IUserLoginStore<TUser>, IUserClaimStore<TUser>, IUserRoleStore<TUser>,
        IUserPasswordStore<TUser>, IUserSecurityStampStore<TUser> where TUser : IdentityUser
    {
        private IEntityStore<TUser> _userStore;
        private readonly IEntityStore<IdentityRole> _roleStore;
        private readonly IEntityStore<IdentityUserLogin> _logins;
        private readonly IEntityStore<IdentityUserClaim> _userClaims;
        private bool _disposed;

        public UserStore(ISession session)
            : this(
                new EntityStore<TUser>(session), new EntityStore<IdentityRole>(session),
                new EntityStore<IdentityUserLogin>(session), new EntityStore<IdentityUserClaim>(session))
        {
            if (session == null)
                throw new ArgumentNullException("session");

            Session = session;
        }

        internal UserStore(IEntityStore<TUser> userStore, IEntityStore<IdentityRole> roleStore,
            IEntityStore<IdentityUserLogin> logins, IEntityStore<IdentityUserClaim> userClaims)
        {
            _userStore = userStore;
            _roleStore = roleStore;
            _logins = logins;
            _userClaims = userClaims;
        }

        public ISession Session { get; private set; }

        private void ThrowIfDisposed()
        {
            if (_disposed)
                throw new ObjectDisposedException(GetType().Name);
        }

        public void Dispose()
        {
            _disposed = true;
            if (Session != null)
            {
                Session.Dispose();
                Session = null;
            }
            _userStore = null;
        }

        public async Task CreateAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            await _userStore.Save(user);
        }

        public async Task UpdateAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            await _userStore.Save(user);
        }

        public async Task DeleteAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            await _userStore.Delete(user);
        }

        public async Task<TUser> FindByIdAsync(string userId)
        {
            ThrowIfDisposed();

            IQueryable<TUser> result = await _userStore.Records();
            return result.SingleOrDefault(w => w.Id == userId);
        }

        public async Task<TUser> FindByNameAsync(string userName)
        {
            ThrowIfDisposed();

            IQueryable<TUser> result = await _userStore.Records();
            return result.SingleOrDefault(w => w.UserName == userName);
        }

        public Task AddLoginAsync(TUser user, UserLoginInfo login)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");
            if (login == null)
                throw new ArgumentNullException("login");

            user.Logins.Add(new IdentityUserLogin
            {
                User = user,
                ProviderKey = login.ProviderKey,
                LoginProvider = login.LoginProvider
            });
            return Task.FromResult(0);
        }

        public async Task RemoveLoginAsync(TUser user, UserLoginInfo login)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");
            if (login == null)
                throw new ArgumentNullException("login");

            IdentityUserLogin entity =
                user.Logins.SingleOrDefault(
                    l =>
                        l.LoginProvider == login.LoginProvider && l.User.Id == user.Id &&
                        l.ProviderKey == login.ProviderKey);
            if (entity != null)
            {
                user.Logins.Remove(entity);
                await _logins.Delete(entity);
            }
        }

        public Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            // Passed on BETA release
            var result =
                (IList<UserLoginInfo>)
                    user.Logins.Select(s => new UserLoginInfo(s.LoginProvider, s.ProviderKey)).ToList();
            return Task.FromResult(result);
        }

        public async Task<TUser> FindAsync(UserLoginInfo login)
        {
            ThrowIfDisposed();
            if (login == null)
                throw new ArgumentNullException("login");

            IQueryable<IdentityUserLogin> logins = await _logins.Records();
            IdentityUser entity =
                logins.Where(
                    l => l.LoginProvider == login.LoginProvider && l.ProviderKey == login.ProviderKey)
                    .Select(l => l.User).FirstOrDefault();
            return entity as TUser;
        }

        public Task<IList<Claim>> GetClaimsAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            // Passed on BETA release
            var result =
                (IList<Claim>)
                    user.Claims.Select(s => new Claim(s.ClaimType, s.ClaimValue)).ToList();
            return Task.FromResult(result);
        }

        public Task AddClaimAsync(TUser user, Claim claim)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");
            if (claim == null)
                throw new ArgumentNullException("claim");

            user.Claims.Add(new IdentityUserClaim
            {
                User = user,
                ClaimType = claim.Type,
                ClaimValue = claim.Value
            });
            return Task.FromResult(0);
        }

        public async Task RemoveClaimAsync(TUser user, Claim claim)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");
            if (claim == null)
                throw new ArgumentNullException("claim");

            foreach (
                IdentityUserClaim entity in
                    user.Claims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToList())
            {
                user.Claims.Remove(entity);
                await _userClaims.Delete(entity);
            }
        }

        public async Task AddToRoleAsync(TUser user, string role)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");
            if (string.IsNullOrWhiteSpace(role))
                throw new ArgumentException(IdentityResources.ValueCannotBeNullOrEmpty, "role");

            IQueryable<IdentityRole> result = await _roleStore.Records();

            IdentityRole identityRole =
                result.SingleOrDefault(
                    r => r.Name.ToUpper() == role.ToUpper());
            if (identityRole == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                    IdentityResources.RoleNotFound, new object[]
                    {
                        role
                    }));
            }
            user.Roles.Add(identityRole);
            //await _userStore.Save(user);
        }

        public async Task RemoveFromRoleAsync(TUser user, string role)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");
            if (string.IsNullOrWhiteSpace(role))
                throw new ArgumentException(IdentityResources.ValueCannotBeNullOrEmpty, "role");

            IdentityRole entity = user.Roles.FirstOrDefault(r => r.Name.ToUpper() == role.ToUpper());
            if (entity != null)
            {
                user.Roles.Remove(entity);
                await _userStore.Save(user);
            }
        }

        public Task<IList<string>> GetRolesAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            return Task.FromResult((IList<string>) user.Roles.Select(u => u.Name).ToList());
        }

        public Task<bool> IsInRoleAsync(TUser user, string role)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");
            if (string.IsNullOrWhiteSpace(role))
                throw new ArgumentException(IdentityResources.ValueCannotBeNullOrEmpty, "role");

            return Task.FromResult(user.Roles.Any(r => r.Name.ToUpper() == role.ToUpper()));
        }

        public async Task SetPasswordHashAsync(TUser user, string passwordHash)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            user.PasswordHash = passwordHash;
            await _userStore.Save(user);
        }

        public Task<string> GetPasswordHashAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            return Task.FromResult(user.PasswordHash);
        }

        public Task<bool> HasPasswordAsync(TUser user)
        {
            return Task.FromResult(user.PasswordHash != null);
        }

        public async Task SetSecurityStampAsync(TUser user, string stamp)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            user.SecurityStamp = stamp;
            await _userStore.Save(user);
        }

        public Task<string> GetSecurityStampAsync(TUser user)
        {
            ThrowIfDisposed();
            if (user == null)
                throw new ArgumentNullException("user");

            return Task.FromResult(user.SecurityStamp);
        }
    }
}